注册、登录、JWT 鉴权、密码加密的思路
注册环节
- 第一步:客户端接口请求
shell
# 执行:使用 API-POST 工具调用注册接口
http://localhost:3000/api/v1/auth/register- 第二步:进入 src/auth/auth.controller.ts 控制器
shell
# 执行:
# 1. 调用 @Public() 装饰器:注入元信息,跳过 JWT 全局守卫拦截
# 2. 调用 register 方法:执行 authService 的注册方法
@Public() # 不加 jwt 拦截
@Post('/register')
async register(@Body() userDto: any) {
const { username, password } = userDto;
return this.authService.register(username, password);
}- 第三步:进入 src/auth/auth.service.ts 服务
shell
# 执行:
# 1. 调用 register 方法,调用 src/user/user.service.ts 中的用户查询和保存方法完成校验和入库
# --> 在入库前完成密码的 argon2 加密
async register(uname: string, pwd: string): Promise<any> {
const user = await this.userService.findOne(uname);
if (user) {
throw new ForbiddenException('此用户已经存在');
}
# argon2 加密
const hashedPassword = await this.passwordService.hashPassword(pwd);
const result = await this.userService.create({
username: uname,
password: hashedPassword,
});
if (result && result.username) {
return {
message: '注册成功',
data: {
id: result.id,
username: result.username,
},
};
}
}- 第四步:结果
shell
{
"message": "注册成功",
"data": {
"id": 7,
"username": "aliswsssas"
}
}登录环节
面向切面编程思路:登录守卫 + passport 协议 + jwt
- 第一步:客户端接口请求
shell
# 执行:使用 API-POST 工具调用登录接口
http://localhost:3000/api/v1/auth/login- 第二步:进入 src/auth/auth.controller.ts 控制器
shell
# 执行:
# 1. 调用 @Public() 装饰器:注入元信息,跳过 JWT 全局守卫拦截
# 2. 调用 LoginAuthGuard 登录守卫:通过 passport 调用 src/auth/login.strategy.ts 的 validate 方法
@UseGuards(LoginAuthGuard)
@Public()
@Post('/login')
async login(@Request() req: any) {
return this.authService.login(req.user);
}- 第三步:进入 src/auth/auth.service.ts 服务
shell
# 调用 src/auth/auth.service.ts 的 validateUser 方法,传入 req 作为参数:调用 src/user/user.service.ts 中的用户查询方法,获取用户信息并校验用户
# --> 调用密码验证方法 verifyPassword: 把客户端的密码和数据库获取的密码传入 verifyPassword 验证密码是否正确
# --> 错误则抛出异常、正确则返回用户信息总除了密码以外的所有值
# --> return result; 的结果会挂载到 req 上通过 req.user 访问
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userService.findOne(username);
if (!user) {
throw new UnauthorizedException('用户不存在');
}
const isValid = await this.passwordService.verifyPassword(
user.password,
pass,
);
if (!isValid) {
throw new UnauthorizedException('密码错误');
}
const { password, ...result } = user;
return result;
}- 第四步:进入 src/auth/auth.controller.ts 控制器
shell
# 调用 src/auth/auth.controller.ts 中的 login 方法:此时 req 会挂载 user 参数,值为 src/auth/auth.service.ts 的 validateUser 方法的返回值,即:用户信息
async login(@Request() req: any) {
return this.authService.login(req.user);
}- 第五步:进入 src/auth/auth.service.ts 服务
shell
# 调用 src/auth/auth.service.ts 中的 login 方法并传入 用户信息
# --> 结合用户信息,生成 JWT token,并返回给客户端
async login(user: any) {
const payload = { username: user.username, sub: user.id };
return {
username: payload.username,
access_token: `Bearer ${this.jwtService.sign(payload)}`,
};
}- 第六步:结果
shell
{
"username": "alias",
"access_token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFsaWFzIiwic3ViIjozLCJpYXQiOjE3NjQ5MzcwODcsImV4cCI6MTc2NTAyMzQ4N30.tocUiatXo2GjMlJaUNQlFIgfJ9EgLbs8Duv48p4t584"
}JWT 鉴权
- 第一步:登录获取 token
shell
# 登录接口
http://localhost:3000/api/v1/auth/login
# 登录参数
{
"username": "alias",
"password": "123456"
}
# 获取 toke
{
"username": "alias",
"access_token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFsaWFzIiwic3ViIjozLCJpYXQiOjE3NjQ5Mzc0NTUsImV4cCI6MTc2NTAyMzg1NX0.IKbCW9wOVy5TWv8ptq60IojHLD5m2KAOohGu97LiFfU"
}- 第二步:调用需要 JWT 鉴权的接口,并传入 token
shell
# 调用接口
http://localhost:3000/api/v1/auth/getUser
# 传入 token
Headers 中添加 Authorization 属性,值为 access_token- 第三步:进入 src/auth/auth.controller.ts 控制器
ts
// 执行:默认触发 jwt 全局路由守卫
@Get('/getUser')
async findOne(@Request() req: any) {
return await this.authService.findOne(req.username);
}- 第四步:触发 src/app.module.ts 中注册的 JWT 全局守卫 JwtAuthGuard
shell
# 执行:注册 jwt 全局路由守卫
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
# 注册 jwt 全局路由守卫
const JwtGlobalGuard = {
provide: APP_GUARD,
useClass: JwtAuthGuard,
};
@Module({
providers: [JwtGlobalGuard, AppService],
})- 第五步:进入 src/auth/jwt-auth.guard.ts 守卫
shell
# 执行:
# 1. 判断当前路由上是否存在 @Public() 元信息装饰器,存在则跳过 JWT 守卫
# --> 不存在则执行 src/auth/jwt.strategy.ts 策略
import { Injectable,ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../meta'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}- 第六步:进入 src/auth/jwt.strategy.ts 策略
shell
# 执行:
# 1. 此处 JWT 鉴权逻辑已经高度集成,通过向 super 传入指定选项,完成 JWT 鉴权
# 2. 选项:jwtFromRequest 为从请求中获取 token 、secretOrKey 为自定义的 JWT 加密密钥
# 3. 如果 JWT 校验失败则抛出异常,如果成功则,调用 validate 方法,参数为从 token 中解析出的用户信息
# --> 由于登录时存入 token 中的信息是 { username: user.username, sub: user.id },
# --> 所以最终返回结果为 { userId: payload.sub, username: payload.username }
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}- 第七步:进入 src/auth/auth.controller.ts 控制器
ts
// 执行:
// 1. 能来到这说明已经通过 JWT 鉴权,并已经获取到了用户信息,且已经将信息挂载到 req 上
// 2. 将解析出的用户信息传入 src/auth/auth.service.ts 中的 findOne 方法
@Get('/getUser')
async findOne(@Request() req: any) {
return await this.authService.findOne(req.username);
// 如果通过了 jwt 全局守卫,则可以从 jwt 中解析用户信息,结果是jwt.strategy.ts中 validate 方法的返回值
// req.username: '赵毅'
// req.userId: 1
// 其他 http 请求方式参数,也可以通过这一方式获取,当然也可以从 @Param() @Body @Query 等装饰器中获取http 对应方法的参数
// req.params - 路由参数
// req.query - 查询参数
// req.body - 请求体
// req.headers - 请求头
// req.method - HTTP 方法
// req.url - 请求 URL
}- 第八步:src/auth/auth.service.ts 服务
shell
# 执行:
# 1. 执行 findOne 方法
async findOne(username: string): Promise<User> {
return await this.userService.findOne(username);
}
# 2. 进入 src/user/user.service.ts 服务中,调用 findOne 方法,将结果返回给客户端
async findOne(username: string): Promise<User> {
const options: FindOneOptions<User> = {
where: { username },
};
return await this.userRepository.findOne(options);
}- 第九步:结果
shell
{
"id": 1,
"username": "shangsan",
"password": "$argon2id$v=19$m=65536,t=3,p=4$zOqN/Vv2LoglGRvVV8PMTA$MolXtGP830dCMOWcIc61p5RhEaSavzjn6X8r/UeT4CM",
"createdAt": "2025-06-06T06:56:47.909Z",
"updatedAt": "2025-06-06T06:56:47.909Z",
"deletedAt": null,
"version": 1
}密码加密环节
加解密算法为 argon2,此法:加解密无需密钥,只是解密的时候需要通过传入加密和解密的密码即可,能解开说明密码正确,否则错误
- 注册的时候加密,在 src/auth/auth.service.ts 中
shell
#注册 & 密码加密 argon2
async register(uname: string, pwd: string): Promise<any> {
const user = await this.userService.findOne(uname);
if (user) {
throw new ForbiddenException('此用户已经存在');
}
# argon2 加密
const hashedPassword = await this.passwordService.hashPassword(pwd);
const result = await this.userService.create({
username: uname,
password: hashedPassword,
});
if (result && result.username) {
return {
message: '注册成功',
data: {
id: result.id,
username: result.username,
},
};
}
}- 登录的时候解密,同样在 src/auth/auth.service.ts 中
shell
# 登录 & 密码解析
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userService.findOne(username);
if (!user) {
throw new UnauthorizedException('用户不存在');
}
const isValid = await this.passwordService.verifyPassword(
user.password,
pass,
);
if (!isValid) {
throw new UnauthorizedException('密码错误');
}
const { password, ...result } = user;
return result;
}总结
- 关于用户登录 生成 token 全流程(守卫 + passport 协议 + jwt)
shell
注意:实际上在 auth.service.ts 中即可实现 jwt 签名和校验,用守卫&passport实际上只是一个例子,模仿 token 校验全过程
第一步:
在 auth.controller.ts 路由匹配后,不执行 login 方法,而是首先执行 @UseGuards(LoginAuthGuard) 装饰器,
并将 @Request() req :any 参数传递给 @UseGuards(LoginAuthGuard) 装饰器,
然后此装饰器会触发 login.strategy.ts 下的 validate 方法,并将 @Request() req :any 中的 body 参数传递给 validate 方法,
然后在此 validate 方法中执行数据库查询方法,获取失败后会被拦截,获取成功后,返回用户信息
,注意啦,此用户信息会与原来的@Request() req :any组合,然后返回给 auth.controller.ts 的 login 方法
第二步:
开始执行 auth.controller.ts 中的 login 方法,并从 req.user 中获取到成功状态的 user 用户信息,
然后将此信息传递给 auth.service.ts 中的 login 方法
第三步:
在auth.service.ts 中的 login 方法中 将用户信息塞入 jwt,执行 jwt 签名后,将生成的 token 返回给 auth.controller.ts,返回给 用户
第四步:
使用 API Post 工具测试,在 headers 中新增参数名 Authorization,值为 Bearer + 空格符 + 生成的 token- 关于 token 校验全过程(与上方登录类似)
shell
第一步:
在 auth.controller.ts 路由匹配后,不执行 findOne 方法,而是首先执行 @UseGuards(JwtAuthGuard) 装饰器,
并将 @Request() req :any 参数传递给 @UseGuards(JwtAuthGuard) 装饰器,
然后此装饰器会触发 jwt.strategy.ts 下的 JwtStrategy 类下的一系列内部解析方法处理,大概流程是:获取 header 中的Authorization,然后解析 token
(所谓的内部实现就是封装在 passport 插件内部我们无法看到,这也是借用插件的好处,很多事不需要我们自己做),
解析失败报异常,解析成功则将结果 —— 用户信息 传递给 validate 方法,并 return
,注意啦,此用户信息会与原来的@Request() req :any组合,然后返回给 auth.controller.ts 的 findOne 方法
第二步:
开始执行 auth.controller.ts 中的 findOne 方法,并从 req.user 中获取到成功状态的 user 用户信息,
然后将此信息传递给 auth.service.ts 中的 findOne 方法
第三步:
在auth.service.ts 中的 findOne 方法中,执行核心数据库操作逻辑,将结果返回给 auth.controller.ts,返回给 用户
第四步:
使用 API Post 工具测试,在 headers 中新增参数名 Authorization,值为 Bearer + 空格符 + 生成的 token- 关于 token 时效
shell
token 是无状态的,生成后失效取决于过期时间,在开发中会出现:在过期时间内多次生成的 token,都可以作为登录态
不会出现先生成的 token 失效,如果想让先生成的 token 失效,可以在用户表中加一个版本号字段,每次登录加一
然后将此版本号塞入到 token 中,每次解析时,如果发现当前版本号与当前库里的版本号不符合,则报异常,登录失效
或者 通过短期 token 和刷新 token 的机制来规避风险